package com.htfyun.eink.pw.draw.pen.epd.brush;


import com.htfyun.eink.pw.draw.pen.epd.EPDBaseManager;
import com.htfyun.eink.pw.draw.pen.epd.data.NeoRenderPoint;
import com.htfyun.eink.pw.draw.pen.epd.data.PenHelper;
import com.htfyun.eink.pw.draw.pointer.PWPointer;

import java.util.List;

public class BrushManager extends EPDBaseManager {

    private final BrushParam param = new BrushParam();

    @Override
    public void setStrokeWidth(float strokeWidth) {
        param.setStrokeWidth(strokeWidth);
    }

    @Override
    public void clear() {
        super.clear();
        param.validTouchPoints.clear();
    }

    @Override
    public void onDown(PWPointer point) {

        param.canDrawStroke = false;

        param.sizeDiff = 0;
        param.timestamp = point.getEventTime();


        float pow = (float) Math.pow(point.pressure, param.pressureExponent);
        float width = param.width;
        float size = param.sizeFactor * (width * pow);
        float maxSize = width + 3.0f;
        if (size >= maxSize) {
            size = maxSize;
        }

        param.sizeResult = Math.max(size, 1.0f);

        param.validTouchPoints.add(point);

        renderPoints.add(createRenderPointerBy(point, param.sizeResult));

    }

    @Override
    public void onMove(PWPointer currentPoint) {
        List<PWPointer> points = param.validTouchPoints;
        if (points.isEmpty()) {
            return;
        }

        PWPointer point0 = points.get(0);
        float dx = currentPoint.x - point0.x;
        float dy = currentPoint.y - point0.y;
        float distance = (float) Math.hypot(dx, dy);
        if (param.canDrawStroke || distance > 3.0f) {
            param.canDrawStroke = true;
            drawStrokeByLastMovePoint(param);
            param.validTouchPoints.add(currentPoint);
            return;
        }

        float timestampDiff;
        if (param.useFixedTimestampDiff) {
            timestampDiff = param.fixedTimestampDiff;
        } else {
            timestampDiff = currentPoint.getEventTime() - param.timestamp;
        }
        param.timestamp = currentPoint.getEventTime();

        float width = param.width;

        float maxSize = width + 3.0f;
        float size = param.sizeResult + (width * ((param.timestampFactor * timestampDiff) / param.timestampDivisor));
        if (size >= maxSize) {
            size = maxSize;
        }
        float sizeResult = Math.max(size, 1.0f);
        param.sizeResult = sizeResult;

        float pow = (float) Math.pow(currentPoint.pressure, param.pressureExponent);

        float sizeDiff = sizeResult - (width * pow);
        if (sizeDiff >= 3.0F) {
            sizeDiff = 3.0F;
        }
        if (sizeDiff <= (width * (-0.8f))) {
            sizeDiff = width * (-0.8f);
        }

        param.sizeDiff = sizeDiff;

        renderPoints.add(createRenderPointerBy(point0, param.sizeResult));

    }

    @Override
    public void onUp(PWPointer currentPoint) {
        List<PWPointer> points = param.validTouchPoints;
        if (points.isEmpty()) {
            return;
        }

        if (points.size() < 3) {
            PWPointer point = points.get(points.size() - 1);
            float dx = currentPoint.x - point.x;
            float dy = currentPoint.y - point.y;
            float distance = (float) Math.hypot(dx, dy);
            if (distance > param.width) {
                renderPoints.add(createRenderPointerBy(currentPoint, param.sizeResult));
            }
            return;
        }

        if (param.canDrawStroke) {
            drawStrokeByLastMovePoint(param);
            param.validTouchPoints.add(currentPoint);
        }

        PWPointer point0 = points.get(points.size() - 3);
        PWPointer point1 = points.get(points.size() - 2);
        PWPointer point2 = points.get(points.size() - 1);

        float timestampDiffP1P0;
        if (param.useFixedTimestampDiff) {
            timestampDiffP1P0 = param.fixedTimestampDiff;
        } else {
            timestampDiffP1P0 = point1.getEventTime() - point0.getEventTime();
        }

        float dx = point1.x - point0.x;
        float dy = point1.y - point0.y;
        float distance = (float) Math.hypot(dx, dy);
        float velocityP1P0 = distance / timestampDiffP1P0;

        float timestampP2P1;
        if (param.useFixedTimestampDiff) {
            timestampP2P1 = param.fixedTimestampDiff;
        } else {
            timestampP2P1 = point2.getEventTime() - point1.getEventTime();
        }

        dx = point2.x - point1.x;
        dy = point2.y - point1.y;
        distance = (float) Math.hypot(dx, dy);
        float velocityP2P1 = distance / timestampP2P1;

        if ((velocityP2P1 / velocityP1P0) <= param.velocityRatioHigh) {
            float width = param.width;
            float pow = (float) Math.pow(point2.pressure, param.pressureExponent);
            float size = width * pow;
            if (size >= (float) (width + 3.0F)) {
                size = width + 3.0F;
            }

            param.sizeResult = (float) Math.max(size, 1.0);
            NeoRenderPoint renderPoint = (createRenderPointerBy(point2, param.sizeResult));

            if (renderPoints.size() > 0) {
                NeoRenderPoint previousRenderPoint = renderPoints.get(renderPoints.size() - 1);
                List<NeoRenderPoint> ps = PenHelper.generateStrokePoints(previousRenderPoint, renderPoint);
                renderPoints.addAll(ps);
            }
            renderPoints.add(renderPoint);
        } else {
            drawStrokeByLastMovePoint(param);
        }
    }

    private void drawStrokeByLastMovePoint(BrushParam param) {
        List<PWPointer> points = param.validTouchPoints;

        if (points.size() < 2) {
            return;
        }
        NeoRenderPoint renderPoint;
        if (points.size() == 2) {
            renderPoint = createRenderPointerBy(points.get(1), param.sizeResult);
        } else {
            renderPoint = drawStrokeByMovePointVelocityRatio(param);
        }

        if (renderPoint != null) {
            if (renderPoints.size() > 0) {
                NeoRenderPoint previousRenderPoint = renderPoints.get(renderPoints.size() - 1);
                List<NeoRenderPoint> ps = PenHelper.generateStrokePoints(previousRenderPoint, renderPoint);
                renderPoints.addAll(ps);
            }
            renderPoints.add(renderPoint);
        }
    }

    private NeoRenderPoint drawStrokeByMovePointVelocityRatio(BrushParam param) {
        List<PWPointer> points = param.validTouchPoints;
        int size = points.size();
        if (size < 3) {
            return null;
        }

        PWPointer point0 = points.get(size - 3);
        PWPointer point1 = points.get(size - 2);
        PWPointer point2 = points.get(size - 1);

        float timestampDiffP1P0;
        if (param.useFixedTimestampDiff) {
            timestampDiffP1P0 = param.fixedTimestampDiff;
        } else {
            timestampDiffP1P0 = point1.getEventTime() - point0.getEventTime();
        }

        float dx = point1.x - point0.x;
        float dy = point1.y - point0.y;
        float distance = (float) Math.hypot(dx, dy);
        float velocityP1P0 = distance / timestampDiffP1P0;

        float timestampDiffP2P1;
        if (param.useFixedTimestampDiff) {
            timestampDiffP2P1 = param.fixedTimestampDiff;
        } else {
            timestampDiffP2P1 = point2.getEventTime() - point1.getEventTime();
        }

        dx = point2.x - point1.x;
        dy = point2.y - point1.y;
        distance = (float) Math.hypot(dx, dy);
        float velocityP2P1 = distance / timestampDiffP2P1;

        float velocityResult = (velocityP2P1 + (velocityP1P0 * param.pointFactor)) / (float) (param.pointFactor + 1.0F) / velocityP1P0;
        if (velocityP1P0 <= 0.0F) {
            velocityResult = 0.0F;
        }

        float sizeByVelocity = 0.0f;

        if (velocityResult > 0.0) {

            float width = param.width * param.widthFactor;
            double exp = Math.exp(-param.width);
            float calWidth = (float) (1.0 - exp) * width;

            float pow = (float) Math.pow(velocityResult, param.velocityExponent);

            if (velocityResult >= param.velocityRatioLow) {
                if (velocityResult > param.velocityRatioHigh) {
                    sizeByVelocity = (float) (calWidth + (float) (param.velocityPowFactory * pow)) / (-3.0F);
                }
            } else {
                sizeByVelocity = (float) (calWidth + (float) (param.velocityPowFactory * pow)) * 0.5F;
            }
        }

        float width = param.width;
        float sizeDiffResult = sizeByVelocity + param.sizeDiff;
        if (sizeDiffResult >= 3.0F) {
            sizeDiffResult = 3.0F;
        }

        if (sizeDiffResult <= (width * (-0.8F))) {
            sizeDiffResult = width * (-0.8F);
        }

        param.sizeDiff = sizeDiffResult;
        float pressure = (float) ((param.pointFactor * point1.pressure + point2.pressure) / (param.pointFactor + 1.0));
        double pow = Math.pow(pressure, param.pressureExponent);
        float sizeResult = sizeDiffResult + (float) (width * pow);
        float maxSize = width + 3.0F;
        if (sizeResult >= maxSize) {
            sizeResult = maxSize;
        }

        param.sizeResult = (float) Math.max(sizeResult, 1.0);

        return createRenderPointerBy(point2, param.sizeResult);

    }

}
